﻿// (c) Norbert Huffschmid
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media.Animation;
using SpinDotters.Core.InputControl;

namespace SpinDotters.Core
{
    public class SpinPanel : CylinderPanel, IDragControl
    {
        /// <summary>
        /// Event fired when animated spinning has completed.
        /// </summary>
        public event EventHandler SpinningCompleted;

        /// <summary>
        /// Event fired when the dragging condition has changed
        /// </summary>
        public event EventHandler DraggingChanged;


        private Storyboard spinStoryboard;
        private double targetAngle;

#if WINDOWS_PHONE
        private bool fingerWithinControl;
        private bool flickAnimationRunning;
#else
        private bool mouseCaptured;
        private Point mouseMoveStart;
        private double mouseMoveStartAngle;
#endif

        /// <summary>
        /// Dragging dependency property.
        /// </summary>
        public static readonly DependencyProperty DraggingProperty =
            DependencyProperty.Register(
                "Dragging",
                typeof(bool),
                typeof(SpinPanel),
                new PropertyMetadata(false, new PropertyChangedCallback(OnDraggingChanged)));

        /// <summary>
        /// Gets or sets the dragging condition.
        /// </summary>
        public bool Dragging
        {
            get { return (bool)GetValue(DraggingProperty); }
            set { SetValue(DraggingProperty, value); }
        }

        /// <summary>
        /// Constructor
        /// </summary>
        public SpinPanel()
        {
#if WINDOWS_PHONE
            this.ManipulationDelta += new EventHandler<ManipulationDeltaEventArgs>(SpinPanel_ManipulationDelta);
            this.ManipulationCompleted += new EventHandler<ManipulationCompletedEventArgs>(SpinPanel_ManipulationCompleted);
            this.MouseEnter += new MouseEventHandler(SpinPanel_MouseEnter);
            this.MouseLeave += new MouseEventHandler(SpinPanel_MouseLeave);
#else
            this.MouseLeftButtonDown += new MouseButtonEventHandler(SpinPanel_MouseMoveStart);
            this.MouseLeftButtonUp += new MouseButtonEventHandler(SpinPanel_MouseLeftButtonUp);
            this.MouseEnter += new MouseEventHandler(SpinPanel_MouseMoveStart);
            this.MouseMove += new MouseEventHandler(SpinPanel_MouseMove);
#endif
        }

        /// <summary>
        /// Spin to a given panel child
        /// </summary>
        /// <param name="index">Child panel index</param>
        /// <param name="animationTime">Animation time in milliseconds</param>
        public void SpinToChild(int index, int animationTime)
        {
            if (this.Children.Count > 0)
            {
                // snap at next notch
                double floatingIndex = NormalizeAngle(this.SpinAngle) / 360.0 * this.Children.Count;
                double deltaAngle = (index - floatingIndex) * (360.0 / this.Children.Count);
                if (deltaAngle > 180.0)
                    deltaAngle -= 360;
                else if (deltaAngle <= -180.0)
                    deltaAngle += 360;

                // avoid micro spins after spinning was completed
                if ((Math.Abs(deltaAngle) > ((360 / this.Children.Count) / 100)) ||
                    (this.spinStoryboard != null))
                {
                    Spin(deltaAngle, animationTime, EasingMode.EaseInOut);
                }
            }
        }

        /// <summary>
        /// Spins a given number of steps
        /// </summary>
        /// <param name="upwardSteps">Steps to spin upwards (if positive) or downwards (if negative)</param>
        /// <param name="animationTime">Animation time in milliseconds</param>
        public void SpinSteps(int upwardSteps, int animationTime)
        {
            double deltaAngle = (360.0 / this.Children.Count) * upwardSteps + (this.targetAngle - this.SpinAngle);

            Spin(deltaAngle, animationTime, EasingMode.EaseInOut);
        }

        /// <summary>
        /// Stops a running spin animation
        /// </summary>
        public void StopAnimation()
        {
            if (this.spinStoryboard != null)
            {
                this.spinStoryboard.Stop();
                this.spinStoryboard = null;
            }
        }


        protected virtual void OnSpinningCompleted()
        {
            if (SpinningCompleted != null)
                SpinningCompleted(this, EventArgs.Empty);
        }

        protected virtual void OnDraggingChanged()
        {
            if (this.DraggingChanged != null)
                this.DraggingChanged(this, EventArgs.Empty);
        }


        /// <summary>
        /// Spin animated in upward or downward direction
        /// </summary>
        /// <param name="deltaAngle">Delta angle</param>
        /// <param name="animationTime">Animation time in milliseconds</param>
        /// <param name="easeMode">Ease mode of animation</param>
        private void Spin(double deltaAngle, int animationTime, EasingMode easeMode)
        {
            // TODO: move to XAML

            this.targetAngle = this.SpinAngle + deltaAngle;

            DoubleAnimation spinAngleAnimation = new DoubleAnimation();
            spinAngleAnimation.Duration = new Duration(new TimeSpan(0, 0, 0, 0, animationTime));
            spinAngleAnimation.By = deltaAngle;

            ExponentialEase ease = new ExponentialEase();
            ease.Exponent = 3;
            ease.EasingMode = easeMode;
            spinAngleAnimation.EasingFunction = ease;

            // save indication about existing storyboard, if any
            Storyboard runningStoryboard = this.spinStoryboard;

            this.spinStoryboard = new Storyboard();
            this.spinStoryboard.Duration = spinAngleAnimation.Duration;
            this.spinStoryboard.Children.Add(spinAngleAnimation);
            this.spinStoryboard.Completed += new EventHandler(spinStoryboard_Completed);

            Storyboard.SetTarget(spinAngleAnimation, this);
            Storyboard.SetTargetProperty(spinAngleAnimation, new PropertyPath(CylinderPanel.SpinAngleProperty));
#if WPF
            // WPF has a different stop behavior than silverlight!!!
            // we have to remove the hold condition when storyboard has completed!
            // otherwise binding does not work!
            this.spinStoryboard.FillBehavior = FillBehavior.HoldEnd;
#else
            this.spinStoryboard.FillBehavior = FillBehavior.Stop;
#endif
            this.spinStoryboard.Begin();

            // stop previously running storyboard
            if (runningStoryboard != null)
                runningStoryboard.Stop();
        }

        private void spinStoryboard_Completed(object sender, EventArgs e)
        {
#if WPF
            if (this.spinStoryboard != null)
            {
                // remove holdend
                this.BeginAnimation(CylinderPanel.SpinAngleProperty, null);
            }
#endif
            this.SpinAngle = this.targetAngle; // hold value while allowing bind changes

            this.spinStoryboard = null;

#if WINDOWS_PHONE
            if (this.flickAnimationRunning)
            {
                this.flickAnimationRunning = false;
                this.Dragging = false;
            }
#endif
            OnSpinningCompleted();
        }

#if WINDOWS_PHONE

        private void SpinPanel_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
        {
            if (!this.Expanded)
            {
                e.Handled = true;

                if (this.fingerWithinControl)
                {
                    this.Dragging = true;

                    if (this.Orientation == Orientation.Vertical)
                    {
                        this.SpinAngle -= 360.0 * (e.DeltaManipulation.Translation.Y) / (this.wheelDiameter * Math.PI);
                    }
                    else
                    {
                        this.SpinAngle -= 360.0 * (e.DeltaManipulation.Translation.X) / (this.wheelDiameter * Math.PI);
                    }
                }
            }
        }

        private void SpinPanel_ManipulationCompleted(object sender, ManipulationCompletedEventArgs e)
        {
            e.Handled = true;

            if (this.fingerWithinControl)
            {
                double velocity = (this.Orientation == Orientation.Vertical) ?
                    e.FinalVelocities.LinearVelocity.Y : e.FinalVelocities.LinearVelocity.X;

                if (Math.Abs(velocity) > 0)
                {
                    this.flickAnimationRunning = true;

                    Spin(-velocity / 10.0, (int)Math.Abs(velocity) / 3, EasingMode.EaseOut);
                }
                else
                {
                    this.Dragging = false;
                }
            }
        }

        private void SpinPanel_MouseEnter(object sender, MouseEventArgs e)
        {
            if (!this.Expanded)
            {
                this.fingerWithinControl = true;

                this.Dragging = true;
            }
        }

        private void SpinPanel_MouseLeave(object sender, MouseEventArgs e)
        {
            this.fingerWithinControl = false;

            if (!this.flickAnimationRunning)
                this.Dragging = false;
        }
#else
        private void SpinPanel_MouseMoveStart(object sender, MouseEventArgs e)
        {
            // WPF returns true even when left mouse button is NOT pressed!
#if WPF
            if (!this.Expanded && (e.LeftButton == MouseButtonState.Pressed))
#else
            if (!this.Expanded)
#endif // WPF
            {
                if (this.Children.Count > 0)
                {
                    this.mouseCaptured = this.CaptureMouse();

                    if (this.mouseCaptured)
                    {
                        this.Dragging = true;

                        this.mouseMoveStart = e.GetPosition(this);
                        this.mouseMoveStartAngle = this.SpinAngle;
                    }
                }
            }
        }

        private void SpinPanel_MouseMove(object sender, MouseEventArgs e)
        {
            if (this.mouseCaptured)
            {
                Point mousePosition = e.GetPosition(this);

                // check if mouse pointer is inside of the allowed touch corridor
                if (CheckMousePosition(mousePosition))
                {
                    // mouse pointer is inside corridor

                    if (this.Orientation == Orientation.Vertical)
                    {
                        this.SpinAngle = this.mouseMoveStartAngle +
                            360.0 * (this.mouseMoveStart.Y - mousePosition.Y) / (this.wheelDiameter * Math.PI);
                    }
                    else
                    {
                        this.SpinAngle = this.mouseMoveStartAngle +
                            360.0 * (this.mouseMoveStart.X - mousePosition.X) / (this.wheelDiameter * Math.PI);
                    }
                }
                else
                {
                    // mouse pointer is outside of corridor

                    this.Dragging = false;

                    this.ReleaseMouseCapture();
                    this.mouseCaptured = false;
                }
            }
        }

        private void SpinPanel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (this.mouseCaptured)
                this.Dragging = false;

            this.ReleaseMouseCapture();
            this.mouseCaptured = false;
        }

        private bool CheckMousePosition(Point mousePosition)
        {
            if (this.Orientation == Orientation.Vertical)
            {
                // allow one wheel width to the left and to the right
                if ((mousePosition.X > -this.ActualWidth) &&
                    (mousePosition.X <= 2 * this.ActualWidth) &&
                    (mousePosition.Y >= 0) &&
                    (mousePosition.Y <= this.ActualHeight))
                {
                    return true;
                }
            }
            else
            {
                // allow one wheel height up and down
                if ((mousePosition.Y > -this.ActualHeight) &&
                    (mousePosition.Y <= 2 * this.ActualHeight) &&
                    (mousePosition.X >= 0) &&
                    (mousePosition.X <= this.ActualWidth))
                {
                    return true;
                }
            }

            return false;
        }

#endif // WINDOWS_PHONE

        private static void OnDraggingChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            SpinPanel sp = o as SpinPanel;

            if (sp != null)
            {
                if (sp.Dragging)
                {
                    sp.StopAnimation();
                }

                sp.OnDraggingChanged();
            }
        }
    }
}
